package app_kvClient;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import logger.LogSetup;
import org.apache.log4j.Level;
import utils.CommonUtil;
import client.KVStore;
import common.messages.KVMessage;
import common.messages.KVMessageImpl;

public class KVClient {

	private LogSetup clientLogger; // client log

	private KVStore storeServer = null;

	public static void main(String[] args) {
		KVClient client = new KVClient();
		client.startApplication();
	}

	/**
	 * This method launches the application and tries to read commands entered
	 * by the user
	 * 
	 * @param Nothing
	 * @return true
	 * @exception prints
	 *                out an error message if cannot execute readCommands()
	 */
	public boolean startApplication() {
		setupLogger();
		try {
			readCommands();
		} catch (IOException exc) {
			clientLogger.errorLog(Constants.ERRORS.READ_ERROR, exc);
			System.out.println(Constants.ERRORS.READ_ERROR);
		}
		return true;
	}

	/**
	 * This method prompts the user to send a command, reads the command, splits
	 * it in tokens (words) and based on the first token calls the appropriate
	 * method assigned to the command
	 * 
	 * @throws IOException
	 *             when cannot read the command
	 */
	private void readCommands() throws IOException {

		BufferedReader cons = new BufferedReader(new InputStreamReader(
				System.in));
		boolean quit = false;
		while (!quit) {
			System.out.print("EchoClient>");
			String input = "";
			try {
				input = cons.readLine();
			} catch (IOException e) {
				clientLogger.errorLog(Constants.ERRORS.ERROR_INPUT, e);
				e.printStackTrace();
			}
			String[] tokens = input.trim().split("\\s+");
			clientLogger.infoLog("User input: " + input.trim(), null);
			if (input.trim().equals("server")) {
				tokens = new String[] { Constants.COMMANDS.CONNECT,
						Constants.SETTINGS.SERVER_IP,
						"" + Constants.SETTINGS.SERVER_PORT };
			}
			if (tokens[0].equals(Constants.COMMANDS.CONNECT)) {
				commandConnect(tokens);
			} else if (tokens[0].equals(Constants.COMMANDS.SEND)) {
				commandSend(input);
			} else if (tokens[0].equals(Constants.COMMANDS.DISCONNECT)) {
				if (commandDisconnect()) {
					System.out.println("Disconnected successfully!");
				} else {
					System.out.println("No active connection");
				}
			} else if (tokens[0].equals(Constants.COMMANDS.LOG_LEVEL)) {
				commandLogLevel(tokens);
			} else if (tokens[0].equals(Constants.COMMANDS.HELP)) {
				commandHelp(tokens);
			} else if (tokens[0].equals(Constants.COMMANDS.QUIT)) {
				quit = commandQuit(tokens);
			} else if (tokens[0].equals(Constants.COMMANDS.GET)) {
				commandGET(tokens);
			} else if (tokens[0].equals(Constants.COMMANDS.PUT)) {
				commandPUT(tokens);
			} else {
				commandUnknown(tokens);
			}

		}
	}

	private void getStorageServerResponse(KVMessage msg) {
		KVMessageImpl response = (KVMessageImpl) msg;
		byte[] responseBytes = response.toBytes();
		clientLogger.infoLog("Storage Server Response in bytes: "
				+ bytesToString(responseBytes), null);
		clientLogger.infoLog("Storage Server Response: " + msg, null);
		// TODO: print more beautiful the output
		System.out.println(response);
	}

	/**
	 * This method checks for the correctness of the server ip and port sent
	 * from the user
	 * 
	 * @param user
	 *            input
	 * @return true if the input matches the defined conditions
	 * @return false if such conditions are not met
	 */
	private boolean checkIpAndPortFormat(String[] tokens) {
		Pattern pattern = Pattern.compile(Constants.SETTINGS.PATTERN);

		if (tokens.length != 3) // the number of arguments must be 2 (ip and
								// port)
			return false;
		if (tokens[1] == null || tokens[2] == null)
			// for a successful connection attempt the user must provide both ip
			// and port number
			return false;
		Matcher matcher = pattern.matcher(tokens[1]);
		if (!matcher.matches()) // the ip format must be: x.x.x.x
			return false;

		String serverIp = tokens[1]; // Constants.SETTINGS.SERVER_IP;
		int serverPort = 0;
		try {
			serverPort = Integer.parseInt(tokens[2]);
		} catch (Exception ex) {
			return false;
		}
		return true;
	}

	/**
	 * This method establishes the connection with the server
	 * 
	 * @param user
	 *            input
	 * @return true if the input format is ok and the connection succeeds
	 * @return false if the format check fails or if already connected
	 * @exception when
	 *                connection cannot be established
	 */

	private boolean commandConnect(String[] tokens) throws IOException {
		boolean formatOk = checkIpAndPortFormat(tokens);
		if (!formatOk) {
			incorrectConnect();
			return false;
		}
		if (storeServer != null) {
			clientLogger.warnLog(Constants.ERRORS.CONNECTED, null);
			return false;
		}
		
		try {
			String serverIp = tokens[1];
			int serverPort = Integer.parseInt(tokens[2]);
			storeServer = new KVStore(serverIp, serverPort);
			storeServer.connect();
			boolean connectionSucceed = true;
			if (connectionSucceed) {

				clientLogger.infoLog("Successfully connected to " + serverIp
						+ " :" + serverPort, null);

				return true;
			} else {
				clientLogger.warnLog("Unable to connect to " + serverIp + " :"
						+ serverPort, null);
				System.out.println(Constants.ERRORS.CONNECTION_NOT_SUCCEEDED);
				return false;
			}
		} catch (SocketTimeoutException exc) {
			clientLogger.errorLog(Constants.ERRORS.SOCKET_TIMEOUT, exc);
			System.out.println(Constants.ERRORS.SOCKET_TIMEOUT);
			return false;
		} catch (Exception exc) {
			clientLogger.errorLog(Constants.ERRORS.SOCKET_INITIALIZATION, exc);
			System.out.println(Constants.ERRORS.CONNECTION_NOT_SUCCEEDED);
			return false;
		}

	}

	private void incorrectConnect() {
		System.out.println(Constants.ERRORS.INVALID_ARGUMENT);
		clientLogger.warnLog("During connection establishing: "
				+ Constants.ERRORS.INVALID_ARGUMENT, null);
	}

	private boolean commandDisconnect() {
		if (storeServer != null) {
			storeServer.disconnect();
			clientLogger.infoLog(Constants.MESSAGES.SUCCESS_DISCONNECT, null);
			storeServer = null;
			return true;
		}
		clientLogger.infoLog(Constants.ERRORS.NO_CONNECTION, null);
		return false;
	}

	/**
	 * Checks the SEND command parameters. The command should start without
	 * whitespace. The message should be in size less than 128kBytes.
	 * 
	 * @param user
	 *            input
	 * @return
	 */
	private boolean checkSendParameters(String input) {
		String inputMessage = input.trim();
		if (inputMessage.length() < 5)
			return false;
		inputMessage = inputMessage.substring(5); // send_
		byte[] inputMessageBytes;
		try {
			inputMessageBytes = inputMessage.getBytes("US-ASCII");
		} catch (UnsupportedEncodingException exc) {
			clientLogger.errorLog(
					"Caught during convering the message to ASCII bytes", exc);
			return false;
		}
		if (inputMessageBytes.length > 128 * 1024) {
			System.out
					.println("the server accepts no more than 128 kBytes at once");
			return false;
		}
		return true;
	}

	/**
	 * Handles the SEND command. Checks the SEND parameters. If the format is
	 * ok, and the connection is active, sends the user message to the server
	 * 
	 * @param user
	 *            input
	 * @return
	 * @exception if
	 *                the argument is invalid
	 */
	private boolean commandSend(String input) {
		boolean formatOk = checkSendParameters(input);
		if (!formatOk) {
			incorrectSent();
			clientLogger.warnLog("Invalid format of the message to send", null);
			return false;
		}

		if (storeServer == null) {
			System.out.println(Constants.ERRORS.NO_CONNECTION);
			clientLogger.warnLog(Constants.ERRORS.NO_CONNECTION, null);
			return false;
		}
		try {
			String inputMessage = input.substring(input.indexOf("send ") + 5);
			KVMessageImpl message = (KVMessageImpl) storeServer.put(
					inputMessage, "");
			getStorageServerResponse(message);
		} catch (Exception exc) {
			clientLogger
					.errorLog(
							"Caught during converting the message to bytes and sending",
							exc);
			System.out.println(Constants.ERRORS.INVALID_ARGUMENT);
			return false;
		}
		return true;
	}
	/**
	 * This method prints out an error message when the argument sent by the
	 * user is invalid
	 */
	private void incorrectSent() {
		System.out.println(Constants.ERRORS.INVALID_ARGUMENT);
		clientLogger.warnLog("During sending:"
				+ Constants.ERRORS.INVALID_ARGUMENT, null);
	}

	/**
	 * This method handles the command quit. It simply disconnects and
	 * terminates the application
	 * 
	 * @param tokens
	 * @return
	 */
	private boolean commandQuit(String[] tokens) {
		commandDisconnect();
		System.out.println(Constants.MESSAGES.APPLICATION_QUIT);
		return true;
	}

	/**
	 * This method handles the command help and prints out the explanation for
	 * every command as requested by the user input
	 * 
	 * @param tokens
	 * @return
	 */
	private boolean commandHelp(String[] tokens) {

		if (tokens.length >= 2) {
			if (tokens[1].equals(Constants.COMMANDS.CONNECT)) {
				System.out.println(Constants.HELP.CONNECT);
				return false;
			}
			if (tokens[1].equals(Constants.COMMANDS.DISCONNECT)) {
				System.out.println(Constants.HELP.DISCONNECT);
				return false;
			}
			if (tokens[1].equals(Constants.COMMANDS.SEND)) {
				System.out.println(Constants.HELP.SEND);
				return false;
			}
			if (tokens[1].equals(Constants.COMMANDS.QUIT)) {
				System.out.println(Constants.HELP.QUIT);
				return false;
			}
			if (tokens[1].equals(Constants.COMMANDS.LOG_LEVEL)) {
				System.out.println(Constants.HELP.LOG_LEVEL);
				return false;
			}
			if (tokens[1].equals(Constants.COMMANDS.HELP)) {
				System.out.println(Constants.HELP.HELP);
				return false;
			}
			if (tokens[1].equals(Constants.COMMANDS.PUT)) {
				System.out.println(Constants.HELP.PUT);
				return false;
			}
			if (tokens[1].equals(Constants.COMMANDS.GET)) {
				System.out.println(Constants.HELP.GET);
				return false;
			}
		}
		System.out.println("Syntax: \n" + Constants.HELP.CONNECT
				+ Constants.HELP.DISCONNECT + Constants.HELP.SEND
				+ Constants.HELP.QUIT + Constants.HELP.GET + Constants.HELP.PUT
				+ Constants.HELP.HELP + Constants.HELP.LOG_LEVEL);
		return false;
	}

	/**
	 * This method handles the command for setting the log level
	 * 
	 * @param tokens
	 * @exception when
	 *                the argument is invalid
	 */

	private boolean commandLogLevel(String[] tokens) {
		if (tokens.length != 2) {
			clientLogger.errorLog(Constants.ERRORS.INVALID_ARGUMENT, null);
			System.out.println(Constants.ERRORS.INVALID_ARGUMENT);
			return false;
		}
		try {
			if (tokens[1].trim().equals("-i")) {
				System.out.println("logLevel = " + clientLogger.getLogLevel());
				return false;
			}
			Level logLevel = CommonUtil.getLoggingLevel(tokens[1]);
			clientLogger.infoLog("logger level is changing from "
					+ clientLogger.getLogLevel() + " to " + tokens[1], null);
			if (clientLogger.setLogLevel(logLevel, tokens[1])) {
				System.out.println("logging level set to "
						+ clientLogger.getLogLevel().toString());
				clientLogger.infoLog("logging level set to "
						+ clientLogger.getLogLevel().toString(), null);
			} else {
				System.out
						.println("ERROR:Required log level does not exist! Log level is set to ALL by default");
				clientLogger
						.errorLog(
								"ERROR: Required log level does not exist! Log level is set to ALL by default",
								null);
			}

		} catch (Exception exc) {
			clientLogger.warnLog(exc.getMessage().toString(), exc);
			System.out.print(Constants.ERRORS.INVALID_ARGUMENT);
			return false;
		}
		return false;
	}

	/**
	 * This method handles the unknown commands, by printing an error message
	 * 
	 * @param tokens
	 * @return
	 */
	private boolean commandUnknown(String[] tokens) {
		clientLogger.warnLog("Unrecognized command provided", null);
		System.out.println(Constants.ERRORS.UNRECOGNIZED_COMMAND);
		return false;
	}

	private boolean checkKeyParameter(byte[] key) {
		if (key.length > 20)
			return false;
		return true;

	}
    
	private boolean checkValueParameter(byte[] value) {
		if (value.length > 120 * 1024)
			return false;
		return true;
	}

	
	
	private boolean validParametersPUT(String[] tokens) {
		if (tokens.length != 3)
			return false;
		if (tokens[1] == null || tokens[2] == null)
			return false;
		byte[] keyBytes, valueBytes;
		try {
			keyBytes = tokens[1].getBytes("US-ASCII");
			valueBytes = tokens[2].getBytes("US-ASCII");
		} catch (UnsupportedEncodingException exc) {
			clientLogger.errorLog(
					"Caught during convering the message to ASCII bytes", exc);
			return false;
		}
		if (!checkValueParameter(valueBytes)) {
			System.out.println(Constants.ERRORS.WRONG_VALUE_LENGTH);
			return false;
		}
		if (!checkKeyParameter(keyBytes)) {
			System.out.println(Constants.ERRORS.WRONG_KEY_LENGTH);
			return false;
		}
		return true;
	}
	

	public boolean commandPUT(String[] tokens) {
		if (storeServer == null) {
			System.out.println(Constants.ERRORS.NO_CONNECTION);
			clientLogger.warnLog(Constants.ERRORS.NO_CONNECTION, null);
			return false;
		}
		
		if (!validParametersPUT(tokens)) {
			incorrectConnect();
			return false;
		}

		String key = tokens[1];
		String value = tokens[2];
		try {
			KVMessage msg = storeServer.put(key, value);
			getStorageServerResponse(msg);
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}

		return true;
	}


	private boolean validParametersGET(String[] tokens) {
		if (tokens.length != 2)
			return false;
		byte[] keyBytes;
		try {
			keyBytes = tokens[1].getBytes("US-ASCII");
		} catch (UnsupportedEncodingException exc) {
			clientLogger.errorLog(
					"Caught during convering the message to ASCII bytes", exc);
			return false;
		}
		if (!checkKeyParameter(keyBytes)) {
			System.out.println(Constants.ERRORS.WRONG_KEY_LENGTH);
			return false;
		}
		return true;
	}

	private boolean commandGET(String[] tokens) {
		if (storeServer == null) {
			System.out.println(Constants.ERRORS.NO_CONNECTION);
			clientLogger.warnLog(Constants.ERRORS.NO_CONNECTION, null);
			return false;
		}
		String key = "";
		if (!validParametersGET(tokens)) {
			incorrectConnect();
			return false;
		}
		key = tokens[1];
		try {
			KVMessage msg = storeServer.get(key);
			getStorageServerResponse(msg);
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}

		return true;
	}

	private String bytesToString(byte[] responseBytes) {
		String msgBytes = "";
		for (int i = 1; i < responseBytes.length; i++)
			msgBytes += responseBytes[i];
		return msgBytes;
	}

	public void setupLogger() {
		try {
			clientLogger = new LogSetup(Constants.LOGGERS.CLIENT_LOGGER,
					Level.ALL);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

}
